<?php
/**
 * Library Of Shared Code (LOSC)
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Framework
 * @package    Record
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * Base class for Doctrine records
 *
 * @category   LOSC Framework
 * @package    Record
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
abstract class Losc_Record_AbstractRecord extends Doctrine_Record
{
    /**
     * Contains ZF filters for fields
     *
     * @var array
     */
    protected $_zfFilters = array();
    
    /**
     * Contains ZF validators for fields
     *
     * @var array
     */
    protected $_zfValidators = array();
    
    /**
     * Contains ZF validator messages for fields
     *
     * @var array
     */
    protected $_zfValidatorMessages = array();
    
    /**
     * Whether filters and validators are added
     *
     * @var boolean
     */
    private $_zfFiltersValidatorsAdded = false;
    
    /**#@+
     * Overloads method in Doctrine_Record.
     */

    /**
     * Record constructor
     * 
     * @param  Doctrine_Table|null $table       a Doctrine_Table object or null,
     *                                          if null the table object is 
     *                                          retrieved from current
     *                                          connection
     * @param  boolean             $isNewEntry  whether or not this record is 
     *                                          transient
     * @throws Doctrine_Connection_Exception    if object is created using the 
     *                                          new operator and there are no
     *                                          open connections
     * @throws Doctrine_Record_Exception        if the cleanData operation fails
     *                                          somehow
     */
    public function __construct($table = null, $isNewEntry = false)
    {
        parent::__construct($table, $isNewEntry);
        $this->_addFiltersAndValidators();
    }

    /**
     * Hydrates this object from given array
     *
     * @param  array $data  data  
     * @return boolean
     */
    public function hydrate(array $data)
    {
        $this->_addFiltersAndValidators();
        parent::hydrate($data);
    }

    /**
     * Overrides set method to add filtering using Zend_Filter
     *
     * @param  string  $fieldName
     * @param  mixed   $value
     * @param  boolean $load
     * @return Losc_Record_AbstractRecord
     */
    protected function _set($fieldName, $value, $load = true)
    {
        if ($value !== null && !$value instanceof Doctrine_Null &&
            isset($this->_zfFilters[$fieldName])) {
            foreach ($this->_zfFilters[$fieldName] as $filter) {
                $value = $filter->filter($value);
            }
        }
        
        return parent::_set($fieldName, $value, $load);
    }

    /**
     * Saves record
     * 
     * Applies the changes made to this object into database this method is
     * smart enough to know if any changes are made and whether to use INSERT
     * or UPDATE statement.
     *
     * This method also saves the related components.
     *
     * @param Doctrine_Connection $conn  [optional] connection
     * @return void
     */
    public function save(Doctrine_Connection $conn = null)
    {
        try {
            parent::save($conn);
        } catch (Doctrine_Validator_Exception $dve) {
            throw new Losc_Record_ValidationException($this);
        }
    }

    /**
     * Tries to save the object and all its related components.
     * In contrast to Doctrine_Record::save(), this method does not
     * throw an exception when validation fails but returns TRUE on
     * success or FALSE on failure.
     *
     * @param Doctrine_Connection $conn [optional] connection
     * @return boolean  indicating success
     */
    public function trySave(Doctrine_Connection $conn = null)
    {
        try {
            $this->save($conn);
            return true;
        } catch (Losc_Record_ValidationException $lrve) {
            return false;
        }
    }
    
    /**
     * Validates fields using ZF validators instead of Doctrine validators
     *
     * @return boolean
     */
    public function isValid()
    {
        // flush previous messages
        $this->_zfValidatorMessages = array();

        // Run validation process
        $event = new Doctrine_Event($this, Doctrine_Event::RECORD_VALIDATE);
        $this->preValidate($event);
        $this->getTable()->getRecordListener()->preValidate($event);
        
        if (!$event->skipOperation) {
            // loop zf validators
            $this->_loopZfValidators();
            
            $this->validate();
            if ($this->_state == self::STATE_TDIRTY ||
                $this->_state == self::STATE_TCLEAN) {
                $this->validateOnInsert();
            } else {
                $this->validateOnUpdate();
            }
        }

        $this->getTable()->getRecordListener()->postValidate($event);
        $this->postValidate($event);

        // returns true if there are no validation messages
        return count($this->_zfValidatorMessages) < 1;
    }
    
    /**#@+
     * ZF validator and filter extension.
     */
    
    /**
     * Adds filters and validators from table options
     *
     * @return void
     */
    private function _addFiltersAndValidators()
    {
        if ($this->_zfFiltersValidatorsAdded) {
            return;
        }
        
        $table = $this->getTable();
        $cols = $table->getColumns();
        
        foreach ($cols as $col => $options) {
            $field = $table->getFieldName($col);
            
            if (isset($options['filters']) &&
                is_array($options['filters'])) {
                $this->_parseFilters($field, $options['filters']);
            }
            if (isset($options['validators']) &&
                is_array($options['validators'])) {
                $this->_parseValidators($field, $options['validators']);
            }
        }
        
        $this->_zfFiltersValidatorsAdded = true;
    }
    
    /**
     * Parses and adds filters from record's defined fields/columns
     *
     * @param  string $field
     * @param  array  $validators
     * @return void
     */
    private function _parseFilters($field, array $filters)
    {
        foreach ($filters as $filter => $params) {
            // get instantiated filter from object store
            $filter = Losc_Util_ObjectStore::get($filter, $params);
            
            // add filter
            $this->addFilter($field, $filter);
        }
    }
    
    /**
     * Parses and adds validators from record's defined fields/columns
     *
     * @param  string $field
     * @param  array  $validators
     * @return void
     */
    private function _parseValidators($field, array $validators)
    {
        foreach ($validators as $validator => $params) {
            // get instantiated validator from object store
            $validator = Losc_Util_ObjectStore::get($validator, $params);
            
            // ad validator
            $this->addValidator($field, $validator);
        }
    }
    
    /**
     * Loops ZF validators
     *
     * @return void
     */
    private function _loopZfValidators()
    {
        // get fields to validate
        $fields = $this->exists() ? $this->getModified() : $this->getData();
        
        // loop modified fields
        foreach ($fields as $field => $value) {
            if (!isset($this->_zfValidators[$field])) {
                // skip if field has no validators
                continue;
            }
            
            if ($value === null || $value instanceof Doctrine_Null) {
                $table = $this->getTable();
                $col = $table->getColumnName($field);
                $col = $table->getColumnDefinition($col);
                if (array_key_exists('notnull', $col) &&
                    empty($col['notnull'])) {
                    continue;
                }
            }
            
            // loop field validators
            foreach ($this->_zfValidators[$field] as $validator) {
                // check if field is valid for this validator
                if (!$validator->isValid($value)) {
                    // not valid, add error message(s)
                    $this->addError($field, $validator->getMessages());
                }
            }
        }
    }
    
    /**
     * Adds a filter to the given field
     *
     * @param  string                $field
     * @param  Zend_Filter_Interface $filter
     * @return Losc_Record_AbstractRecord
     */
    public function addFilter($field, Zend_Filter_Interface $filter)
    {
        if (!isset($this->_zfFilters[$field])) {
            $this->_zfFilters[$field] = array();
        }
        
        $this->_zfFilters[$field][] = $filter;
        
        return $this;
    }
    
    /**
     * Adds a validator to the given field
     *
     * @param  string                  $field      field name
     * @param  Zend_Validate_Interface $validator  valiator to add to field
     * @return Losc_Record_AbstractRecord                                         
     */
    public function addValidator($field, Zend_Validate_Interface $validator)
    {
        if (!isset($this->_zfValidators[$field])) {
            $this->_zfValidators[$field] = array();
        }
        
        $this->_zfValidators[$field][] = $validator;
        
        return $this;
    }
    
    /**
     * Adds one or several error messages to a field
     *
     * @param string $field          field to add error to
     * @param string $message|array  message to add
     */
    public function addError($field, $message)
    {
        if (!isset($this->_zfValidatorMessages[$field])) {
            $this->_zfValidatorMessages[$field] = array();
        }
        
        if (is_array($message)) {
            foreach ($message as $msg) {
                $this->_zfValidatorMessages[$field][] = $msg;
            }
        } else {
            $this->_zfValidatorMessages[$field][] = $message;
        }
    }
    
    /**
     * Returns validator messages
     *
     * @return array
     */
    public function getValidatorMessages()
    {
        return $this->_zfValidatorMessages;
    }
}
